/*
 * $Id: NativeFont.java,v 1.2 2007/12/20 18:33:31 rbair Exp $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package com.sun.pdfview.font;

import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.OpenType;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import com.sun.pdfview.PDFObject;
import com.sun.pdfview.PDFParseException;
import com.sun.pdfview.font.ttf.CMap;
import com.sun.pdfview.font.ttf.CMapFormat0;
import com.sun.pdfview.font.ttf.CMapFormat4;
import com.sun.pdfview.font.ttf.CmapTable;
import com.sun.pdfview.font.ttf.HeadTable;
import com.sun.pdfview.font.ttf.HmtxTable;
import com.sun.pdfview.font.ttf.NameTable;
import com.sun.pdfview.font.ttf.PostTable;
import com.sun.pdfview.font.ttf.TrueTypeFont;
import com.sun.pdfview.font.ttf.TrueTypeTable;

/**
 * a font object derived from a true type font.
 *
 * @author Mike Wessler
 */
public class NativeFont extends OutlineFont {
    /** Control characters to filter out of the underlying font */
    protected static final char[] controlChars = { 0x9, 0xa, 0xd };
    
    /** the ids of our favorite CMaps */
    protected static final short[] mapIDs = { 
        3, 1, /* Microsoft Unicode */
        0, 0, /* unicode default */
        0, 3, /* unicode 2.0 map */
        1, 0  /* macintosh */
    };
    
    /** the actual font in use */
    private Font f;
    
    /** the font render context */
    private FontRenderContext basecontext = 
        new FontRenderContext(new AffineTransform(), true, true);
    
    /** the cmap table from a TrueType font */
    private CmapTable cmapTable;
    
    /** the post table from a TrueType font */
    private PostTable postTable;
    
    /** the number of font units in one em */
    private int unitsPerEm;
    
    /** the hmtx table from the TrueType font */
    private HmtxTable hmtxTable;
    
    /**
     * create a new NativeFont object based on a description of the
     * font from the PDF file.  If the description happens to contain
     * an in-line true-type font file (under key "FontFile2"), use the
     * true type font.  Otherwise, parse the description for key information
     * and use that to generate an appropriate font.
     */
    public NativeFont(String baseFont, PDFObject fontObj,
                      PDFFontDescriptor descriptor)
	throws IOException 
    {
	super(baseFont, fontObj, descriptor);
        
        String fontName = descriptor.getFontName();
        
	PDFObject ttf = descriptor.getFontFile2();
	if (ttf != null) {
	    byte[] fontdata = ttf.getStream();
	   
	    try {
                setFont(fontdata);
	    } catch (FontFormatException ffe) {
		throw new PDFParseException("Font format exception: "+ffe);
	    }
	} else {
	    int flags = descriptor.getFlags();
	    int style = ((flags & (1<<18))!=0)?Font.BOLD:Font.PLAIN;
	    
	    if (fontName.indexOf("Bold") > 0) {
		style |= Font.BOLD;
	    }
	    if (descriptor.getItalicAngle() != 0) {
		style |= Font.ITALIC;
	    }
	    if ((flags & 1)!=0) { // fixed width
		setFont(new Font("Monospaced", style, 1));
	    } else if ((flags & 1<<1)!=0) {  // serif font
		setFont(new Font("Serif", style, 1));
	    } else {
		setFont(new Font("Sans-serif", style, 1));
	    }
	}
    }
    
    /**
     * Get a glyph outline by name
     *
     * @param name the name of the desired glyph
     * @return the glyph outline, or null if unavailable
     */
    protected GeneralPath getOutline(String name, float width) {        
        if (postTable != null && cmapTable != null) {
            // map this character name to a glyph ID
            short glyphID = postTable.getGlyphNameIndex(name);
            
            if (glyphID == 0) {
                // no glyph -- try by index
                return null;
            }
            
            // the mapped character
            char mappedChar = 0;
            
            for (int i = 0; i < mapIDs.length; i += 2) {
                CMap map = cmapTable.getCMap(mapIDs[i], mapIDs[i + 1]);
                if (map != null) {
                    mappedChar = map.reverseMap(glyphID);
                    
                    // we found a character
                    if (mappedChar != 0) {
                        break;
                    }
                }
            }
            
            return getOutline(mappedChar, width);
        }
        
        // no maps found, hope the font can deal
        return null;
    }
    
    /**
     * Get a glyph outline by character code
     *
     * Note this method must always return an outline 
     *
     * @param src the character code of the desired glyph
     * @return the glyph outline
     */
    protected GeneralPath getOutline(char src, float width) { 
        // some true type fonts put characters in the undefined
        // region of Unicode instead of as normal characters.
        if (!f.canDisplay(src) && f.canDisplay((char) (src + 0xf000))) {
            src += 0xf000;
        }
        
        // filter out control characters
        for (int i = 0; i < controlChars.length; i++) {
            if (controlChars[i] == src) {
                src = (char) (0xf000 | src);
                break;
            }
        }
        
        char[] glyph = new char[1];
	glyph[0] = src;
	
        GlyphVector gv = f.createGlyphVector(basecontext, glyph);
	GeneralPath gp = new GeneralPath(gv.getGlyphOutline(0));
       
        // this should be gv.getGlyphMetrics(0).getAdvance(), but that is
        // broken on the Mac, so we need to read the advance from the
        // hmtx table in the font
        CMap map = cmapTable.getCMap(mapIDs[0], mapIDs[1]);
        int glyphID = map.map(src);
        float advance = (float) hmtxTable.getAdvance(glyphID) / (float) unitsPerEm;
        
        float widthfactor = width / advance;
	gp.transform(AffineTransform.getScaleInstance(widthfactor, -1));
                
        return gp;
    }
    
    /**
     * Set the font
     *
     * @param f the font to use
     */
    protected void setFont(Font f) {
        this.f = f;
    
        // if it's an OpenType font, parse the relevant tables to get
        // glyph name to code mappings
        if (f instanceof OpenType) {
            OpenType ot = (OpenType) f;
            
            byte[] cmapData = ot.getFontTable(OpenType.TAG_CMAP);
            byte[] postData = ot.getFontTable(OpenType.TAG_POST);
            
            TrueTypeFont ttf = new TrueTypeFont(0x10000);
            
            cmapTable = 
                (CmapTable) TrueTypeTable.createTable(ttf, "cmap", 
                                                      ByteBuffer.wrap(cmapData));
            ttf.addTable("cmap", cmapTable);
            
            postTable =
                (PostTable) TrueTypeTable.createTable(ttf, "post",
                                                      ByteBuffer.wrap(postData));
            ttf.addTable("post", postTable);
        }
    }

    /**
     * Set the font
     *
     * @param fontdata the font data as a byte array
     */
    protected void setFont(byte[] fontdata)
        throws FontFormatException, IOException {                          
        
        // System.out.println("Loading " + getBaseFont());
        // FileOutputStream fos = new FileOutputStream("/tmp/" + getBaseFont() + ".ttf");
        // fos.write(fontdata);
        // fos.close();
  
        try {
            // read the true type information
            TrueTypeFont ttf = TrueTypeFont.parseFont(fontdata);
       
            // System.out.println(ttf.toString());
            
            // get the cmap, post, and hmtx tables for later use
            cmapTable = (CmapTable) ttf.getTable("cmap");
            postTable = (PostTable) ttf.getTable("post");
            hmtxTable = (HmtxTable) ttf.getTable("hmtx");
            
            // read the units per em from the head table
            HeadTable headTable = (HeadTable) ttf.getTable("head");
            unitsPerEm = headTable.getUnitsPerEm();
            
            /* Find out if we have the right info in our name table.
            * This is a hack because Java can only deal with fonts that
            * have a Microsoft encoded name in their name table (PlatformID 3).
            * We'll 'adjust' the font to add it if not, and take our chances
            * with our parsing, since it wasn't going to work anyway.
            */
            NameTable nameTable = null;
            
            try {
                nameTable = (NameTable) ttf.getTable("name");
            } catch (Exception ex) {
                System.out.println("Error reading name table for font " + 
                                   getBaseFont() + ".  Repairing!");
            }
            
            boolean nameFixed = fixNameTable(ttf, nameTable);
                 
            /* Figure out if we need to hack the CMap table.  This might
             * be the case if we use characters that Java considers control
             * characters (0x9, 0xa and 0xd), that have to be re-mapped
             */
            boolean cmapFixed = fixCMapTable(ttf, cmapTable);
            
            // use the parsed font instead of the original
            if (nameFixed || cmapFixed) {
                // System.out.println("Using fixed font!");
                // System.out.println(ttf.toString());
                fontdata = ttf.writeFont();
            
                // FileOutputStream fos2 = new FileOutputStream("/tmp/" + getBaseFont() + ".fix");
                // fos2.write(fontdata);
                // fos2.close();
            }
        } catch (Exception ex) {
            System.out.println("Error parsing font : " + getBaseFont());
            ex.printStackTrace();
        }
          
        ByteArrayInputStream bais = new ByteArrayInputStream(fontdata);
        f = Font.createFont(Font.TRUETYPE_FONT, bais);
        bais.close();       
    }
    
    /**
     * Fix a broken font name table for a TrueType font.  Some fonts do not
     * have Microsoft-specific name information, but Java won't work without
     * it (grrr.).  This method takes a font and adds the Microsoft data into
     * it.
     *
     * @param ttf the font
     * @param name the font's name table
     * @return true if the table was fixed, or false if it was left as is
     */
    private boolean fixNameTable(TrueTypeFont ttf, NameTable name) {
        // if we didn't find the table, or there was an exception,
        // just create a new one
        if (name == null) {
            name = (NameTable) TrueTypeTable.createTable(ttf, "name");
            ttf.addTable("name", name);
        }
              
        // first, figure out some info about the font
        String fName = this.getBaseFont();
        String style = "Regular";
        
        if (fName.indexOf("Italic") > -1 || fName.indexOf("italic") > -1) {
            style = "Italic";
        } else if (fName.indexOf("Bold") > -1 || fName.indexOf("bold") > -1) {
            style = "Bold";
        }
        
        if (fName.indexOf('-') > -1) {
            fName = fName.substring(0, fName.indexOf('-'));
        }
        
        short platID = NameTable.PLATFORMID_MICROSOFT;
        short encID  = 1;
        short langID = 1033;
           
        short[] nameIDs = {
            NameTable.NAMEID_COPYRIGHT,
            NameTable.NAMEID_FAMILY,
            NameTable.NAMEID_SUBFAMILY,
            NameTable.NAMEID_SUBFAMILY_UNIQUE,
            NameTable.NAMEID_FULL_NAME,
            NameTable.NAMEID_VERSION,
            NameTable.NAMEID_POSTSCRIPT_NAME,
            NameTable.NAMEID_TRADEMARK
        };
        
        String[] defaultValues = {
            "No copyright",
            fName,
            style,
            fName + " " + style,
            fName + " " + style,
            "1.0 (Fake)",
            fName,
            "No Trademark"
        };
        
        boolean changed = false;
        
        for (int i = 0; i < nameIDs.length; i++) {
            if (name.getRecord(platID, encID, langID, nameIDs[i]) == null) {
                name.addRecord(platID, encID, langID, nameIDs[i],
                               defaultValues[i]);
                changed = true;
            }
        }
        
        return changed;
    }
    
    /**
     * Fix the CMap table.  This can be necessary if characters are mapped to
     * control characters (0x9, 0xa, 0xd) Java will not render them, even 
     * though they are valid.
     *
     * Also, Java tends to not like it when there is only a Format 0 CMap,
     * which happens frequently when included Format 4 CMaps are broken.
     * Since PDF prefers the Format 0 map, while Java prefers the Format 4 map,
     * it is generally necessary to re-write the Format 0 map as a Format 4 map
     * to make most PDFs work.
     *
     * @param ttf the font
     * @param cmap the CMap table
     * @return true if the font was changed, or false if it was left as-is
     */
    private boolean fixCMapTable(TrueTypeFont ttf, CmapTable cmap) {
        CMapFormat4 fourMap = null;
        CMapFormat0 zeroMap = null;
        
        for (int i = 0; i < mapIDs.length; i += 2) {
            CMap map = cmapTable.getCMap(mapIDs[i], mapIDs[i + 1]);
            if (map != null) {
                if (fourMap == null && map instanceof CMapFormat4) {
                    fourMap = (CMapFormat4) map;
                } else if (zeroMap == null && map instanceof CMapFormat0) {
                    zeroMap = (CMapFormat0) map;
                }
            }
        }
            
        // if there were no maps, we could have problems.  Just try creating
        // an identity map
        if (zeroMap == null && fourMap == null) {
            fourMap = (CMapFormat4) CMap.createMap((short) 4, (short) 0);
            fourMap.addSegment((short) getFirstChar(),
                               (short) getLastChar(),
                               (short) 0);
        }
        
        // create our map based on the type 0 map, since PDF seems
        // to prefer a type 0 map (Java prefers a unicode map)
        if (zeroMap != null) {
            fourMap = (CMapFormat4) CMap.createMap((short) 4, (short) 0);
        
            // add the mappings from 0 to null and 1 to notdef
            fourMap.addSegment((short) 0, (short) 1, (short) 0);
            
            for (int i = getFirstChar(); i <= getLastChar(); i++) {
                short value = (short) (zeroMap.map((byte) i) & 0xff);
                if (value != 0) {
                    fourMap.addSegment((short) i, (short) i, (short) (value - i)); 
                }
            }
        }
        
        // now that we have a type four map, remap control characters
        for (int i = 0; i < controlChars.length; i++) {
            short idx = (short) (0xf000 | controlChars[i]);
            short value = (short) fourMap.map(controlChars[i]);
            
            fourMap.addSegment(idx, idx, (short) (value - idx));
        }
        
        // create a whole new table with just our map
        cmap = (CmapTable) TrueTypeTable.createTable(ttf, "cmap");
        cmap.addCMap((short) 3, (short) 1, fourMap);
        
        // replace the table in the font
        ttf.addTable("cmap", cmap);
        
        // change the stored table
        cmapTable = cmap;
        
        return true;
    }
}
